Dependency Injection topic
Dependency injection
Motivation
Dependency injection is a common pattern in software development to decouple components while making them easier to test. Another benefit is that it allows Refena to build a dependency graph.
In Refena, we distinguish between rebuildable and non-rebuildable providers.
Rebuildable providers
Types:
ViewProvider
, FutureProvider
, StreamProvider
Inside the provider lambda of a rebuildable provider,
always use ref.watch
to access other providers.
final userDataRepository = ViewProvider<UserDataRepository>((ref) {
final id = ref.watch(userIdProvider);
return UserDataRepository(id);
});
Non-rebuildable providers
Types:
Provider
, StateProvider
, NotifierProvider
, ReduxProvider
Since they never rebuild themselves,
you can't use ref.watch
inside them.
There are two ways to read other providers:
➤ Non-rebuildable providers
If you want to read non-rebuildable providers,
you can just use ref.read
since the value of these providers never changes.
final settingsProvider = NotifierProvider<SettingsService, SettingsState>((ref) {
// The persistenceProvider is initialized once and never change.
final persistenceService = ref.read(persistenceProvider);
return SettingsService(persistenceService);
});
➤ Rebuildable providers
If you read rebuildable providers (e.g. ViewProvider
),
you should use ref.accessor
to inject a StateAccessor
into the notifier.
It allows you to read the latest state of the provider.
final settingsProvider = NotifierProvider<SettingsService, SettingsState>((ref) {
// Here, the userDataRepository might change
// if the user logs in or out.
final repository = ref.accessor(userDataRepository);
return SettingsService(repository);
});
class SettingsService extends Notifier<SettingsState> {
final StateAccessor<UserDataRepository> repository;
SettingsService(this.repository);
@override
SettingsState init() => SettingsState.initial();
void setLocale(Locale locale) {
// With .state, you can access the latest state of the provider
repository.state.setLocale(locale);
state = state.copyWith(locale: locale);
}
}
Why not always ref.watch?
Refena differentiates between rebuildable and non-rebuildable providers.
There are several reasons for this:
➤ Implicit documentation
Having Provider
and ViewProvider
allows you to see at a glance whether a provider is rebuildable or not.
For example, if you don't expect a singleton to rebuild itself, make it a Provider
.
➤ Notifiers should not rebuild
When you are implementing a notifier method, you might modify the state of an injected provider.
If the NotifierProvider
is rebuildable,
a change of the injected provider will also rebuild the NotifierProvider
itself because
it depends on the injected provider.
This will cause the current instance of the NotifierProvider
to be disposed
(even when the method is not finished yet).
To avoid this, Refena requires most of the providers (especially notifier-oriented providers) to be non-rebuildable.
final childProvider = NotifierProvider<Child, ChildState>((ref) {
// ref.watch will throw a compile-time error
final parentState = ref.read(parentProvider);
return Child(parentState);
});
class Child extends Notifier<ChildState> {
final ParentState parentState;
Child(this.parentState);
@override
ChildState init() => ChildState.initial(
count: parentState.count,
);
void increment() {
// This will rebuild the parentProvider
ref.notifier(parentProvider).increment();
// If the NotifierProvider is rebuildable, this will throw an exception
// because current instance is already disposed.
// To prevent this kind of bug, ref.watch is prohibited in NotifierProvider.
state = state.copyWith(count: state.count + 1);
}
}
Doesn't it introduce bugs?
If you want to change a Provider
to a ViewProvider
,
then all non-rebuildable consumers should use ref.accessor
instead of ref.read
.
This is why Riverpod suggests using ref.watch
everywhere.
However, this is not needed if you have a clear distinction between rebuildable and non-rebuildable providers. This additional type system allows Refena to provide lint rules that prevent this kind of bug.
Summary
Provider | Inject rebuildable | Inject non-rebuildable |
---|---|---|
RebuildableViewProvider , FutureProvider , StreamProvider |
ref.watch |
ref.watch |
Non-rebuildableNotiferProvider , ReduxProvider |
ref.accessor |
ref.read |
Provider |
change to ViewProvider |
ref.read |
Classes
-
StateAccessor<
R> Dependency Injection - Provides access to the latest state of a provider.
-
StateAccessor<
R> Dependency Injection - Provides access to the latest state of a provider.